﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Discovery;
using ServiceModelEx;
using System.IO;
using System.ServiceModel.Channels;

namespace Catalyst.ServiceModel
{
	public class ServiceHostFactory
	{
		public delegate Uri[] BaseAddressLoader();
		public delegate List<ServiceEndpoint> ServiceEndpointLoader();

		public static Uri CreateScope(string scheme, string machineName, string serviceName)
		{
			return new Uri(String.Format("{0}://{1}.{2}", scheme, machineName, serviceName));
		}

		public static ServiceHost CreateDynamicServiceHost(Type serviceType, bool isDiscoverable, bool isMex, BaseAddressLoader baseAddressLoader, ServiceEndpointLoader endpointLoader)
		{
			ServiceHost serviceHost = null;

			if (serviceType.BaseType.IsGenericType && serviceType.BaseType.GetGenericTypeDefinition().UnderlyingSystemType == typeof(ServiceModelEx.DiscoveryPublishService<>))
			{
				serviceHost = createDiscoveryPublishSubscribeHost(serviceType);
			}
			else if (isDiscoverable)
			{
				serviceHost = createDiscoverableHost(serviceType, endpointLoader.Invoke());
			}
			else
			{
				serviceHost = createGenericHost(serviceType, baseAddressLoader.Invoke(), endpointLoader.Invoke(), isMex);
			}

			serviceHost.Description.Behaviors.Find<ServiceDebugBehavior>().IncludeExceptionDetailInFaults = true;

			return serviceHost;
		}

		private static ServiceHost createDiscoveryPublishSubscribeHost(Type serviceType)
		{
			Type publishServiceType = serviceType.BaseType;

			Type contractType = publishServiceType.GetGenericArguments()[0];

			MethodInfo staticCreateHost = publishServiceType.GetMethod("CreateHost").MakeGenericMethod(serviceType);

			object obj = staticCreateHost.Invoke(publishServiceType, null);

			ServiceHost serviceHost = (ServiceHost)obj;

			NetTcpBinding binding = new NetTcpBinding(System.ServiceModel.SecurityMode.Transport, true);
			serviceHost.AddServiceEndpoint(getPingContractType(serviceType), binding, "");

			return serviceHost;
		}

		private static ServiceHost createDiscoverableHost(Type serviceType, List<ServiceEndpoint> endpoints)
		{
			if (endpoints == null || endpoints.Count != 1)
			{
				throw new ServiceActivationException(String.Format("Service {0} must have a cardinality of 1.", serviceType.FullName));
			}

			ServiceHost serviceHost = new ServiceHost(serviceType);
			addServiceEndpoints(serviceHost, serviceType, endpoints);

			Action<ServiceEndpoint> addScope = (e) =>
				{
					Uri endpointAddress = e.Address.Uri;

					EndpointDiscoveryBehavior discoveryBehavior = new EndpointDiscoveryBehavior();
					discoveryBehavior.Scopes.Add(CreateScope(endpointAddress.Scheme, endpointAddress.Host, serviceType.Name));

					e.Behaviors.Add(discoveryBehavior);
				};

			serviceHost.Description.Endpoints.ForEach(addScope);

			serviceHost.AddServiceEndpoint(new UdpDiscoveryEndpoint());
			
			ServiceDiscoveryBehavior behavior = new ServiceDiscoveryBehavior();
			//behavior.AnnouncementEndpoints.Add(new UdpAnnouncementEndpoint());

			serviceHost.Description.Behaviors.Add(behavior);

			return serviceHost;
		}

		private static ServiceHost createGenericHost(Type serviceType, Uri[] baseAddresses, List<ServiceEndpoint> endpoints, bool isMex)
		{
			Type hostType = typeof(ServiceHost<>).MakeGenericType(serviceType);

			object obj = Activator.CreateInstance(hostType, baseAddresses);

			ServiceHost serviceHost = (ServiceHost)obj;

			addServiceEndpoints(serviceHost, serviceType, endpoints);

			if (isMex)
			{
				hostType.GetMethod("EnableMetadataExchange").Invoke(serviceHost, new object[] { true });
			}

			return serviceHost;
		}

		private static void addServiceEndpoints(ServiceHost serviceHost, Type serviceType, List<ServiceEndpoint> endpoints)
		{
			Action<ServiceEndpoint> addServiceEndpoint = (e) =>
			{
				string address = "";

				if (e.Address != null)
				{
					address = e.Address.Uri.AbsoluteUri;
				}
				else if (!(e.Binding is CustomBinding))
				{
					address = serviceType.Name;
				}

				//if (!(e.Binding is CustomBinding))
				//{
					ServiceEndpoint serviceEndpoint = serviceHost.AddServiceEndpoint(e.Contract.ContractType, e.Binding, address);

					if (e.Binding is CustomBinding)
					{
						serviceEndpoint.Behaviors.Add(e.Behaviors[0]);
					}
				
					serviceHost.AddServiceEndpoint(getPingContractType(serviceType), e.Binding, address);
				//}
			};

			endpoints.ForEach(addServiceEndpoint);
		}

		private static Type getPingContractType(Type serviceType)
		{
			Type pingType = serviceType.GetInterface("IPing");

			if (pingType != null && pingType.GetMethod("Ping") != null)
			{
				return pingType;
			}

			throw new ServiceActivationException(String.Format("Service \"{0}\" does not implement contract IPing with a method \"bool Ping()\".", serviceType.FullName));
		}
	}
}
